package lasec.util;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.mahout.cf.taste.common.NoSuchItemException;
import org.apache.mahout.cf.taste.common.NoSuchUserException;
import org.apache.mahout.cf.taste.common.Refreshable;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.impl.common.FastIDSet;
import org.apache.mahout.cf.taste.impl.common.FullRunningAverage;
import org.apache.mahout.cf.taste.impl.common.RunningAverage;
import org.apache.mahout.cf.taste.impl.recommender.AbstractRecommender;
import org.apache.mahout.cf.taste.impl.recommender.TopItems;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.model.Preference;
import org.apache.mahout.cf.taste.recommender.CandidateItemsStrategy;
import org.apache.mahout.cf.taste.recommender.IDRescorer;
import org.apache.mahout.cf.taste.recommender.RecommendedItem;
import org.apache.mahout.math.DenseMatrix;
import org.apache.mahout.math.Matrix;
import org.apache.mahout.math.SingularValueDecomposition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;

public class SSVDRecommender extends AbstractRecommender {

	private static final Logger log = LoggerFactory
			.getLogger(SSVDRecommender.class);

	private long userID;
	private double average;
	private double threshold;
	private double[][] doubleA;

	private VectorSpaceModel vsm;

	private Matrix A;
	private Matrix B;

	private Map<String, Integer> rowLabels;
	private Map<String, Integer> columnLabels;
	private Map<String, Integer> tempColumnLabels;

	public SSVDRecommender(DataModel model, long userID, double threshold) throws TasteException {
		this(model, getDefaultCandidateItemsStrategy(), userID, threshold);
	}

	public SSVDRecommender(DataModel model,
			CandidateItemsStrategy defaultCandidateItemsStrategy, long userID, double threshold)
			throws NumberFormatException, TasteException {
		// TODO Auto-generated constructor stub
		super(model, defaultCandidateItemsStrategy);

		this.userID = userID;
		this.threshold = threshold;
		System.out.println("recommending for userID: " + userID);
		vsm = new VectorSpaceModel(String.valueOf(userID));
		B = vsm.getSimilarUsers();

		int idx = 0;
		tempColumnLabels = B.getColumnLabelBindings();
		Iterator<String> userIterator = tempColumnLabels.keySet().iterator();

		columnLabels = new LinkedHashMap<String, Integer>();
		columnLabels.put(String.valueOf(userID), idx++);

		String uid = "";
		int no = 0;
		double similarity = 0d;
		while (userIterator.hasNext()) {
			uid = userIterator.next();
			no = tempColumnLabels.get(uid);
			similarity = B.get(0, no);
			if (similarity > threshold) {
				columnLabels.put(uid, idx++);
			}
		}

		B = null;
		idx = 0;
		userIterator = columnLabels.keySet().iterator();
		rowLabels = new LinkedHashMap<String, Integer>();
		while (userIterator.hasNext()) {
			for (Preference pref : model.getPreferencesFromUser(Long
					.parseLong(userIterator.next()))) {
				if (!rowLabels.containsKey(String.valueOf(pref.getItemID()))) {
					rowLabels.put(String.valueOf(pref.getItemID()), idx++);
				}
			}
		}

		doubleA = new double[rowLabels.size()][columnLabels.size()];
		A = new DenseMatrix(doubleA);
		A.setRowLabelBindings(rowLabels);
		A.setColumnLabelBindings(columnLabels);

		userIterator = A.getColumnLabelBindings().keySet().iterator();
		while (userIterator.hasNext()) {
			for (Preference pref : model.getPreferencesFromUser(Long
					.parseLong(userIterator.next()))) {
				A.set(String.valueOf(pref.getItemID()),
						String.valueOf(pref.getUserID()), pref.getValue());
			}
		}

		average = getAveragePreference(A);

		A = buildSVDMatrix(A);
		A.setRowLabelBindings(rowLabels);
		A.setColumnLabelBindings(columnLabels);
	}

	private Matrix buildSVDMatrix(Matrix matrix) {
		int k = 0;

		if (matrix.columnSize() >= 10) {
			k = matrix.columnSize() / 3;
		} else {
			k = matrix.columnSize() % 10;
		}

		SingularValueDecomposition svd = new SingularValueDecomposition(matrix);

		Matrix U = svd.getU();
		Matrix Uk = U.viewPart(0, U.rowSize(), 0, k);

		Matrix rootSk = svd.getS().viewPart(0, k, 0, k);
		for (int i = 0; i < rootSk.columnSize(); i++) {
			rootSk.set(i, i, Math.sqrt(rootSk.get(i, i)));
		}

		Matrix V = svd.getV().transpose();
		Matrix Vk = V.viewPart(0, k, 0, V.columnSize());

		Matrix left = Uk.times(rootSk.transpose());
		Matrix right = rootSk.times(Vk);

		Matrix P = left.times(right);

		U = null;
		Uk = null;
		rootSk = null;
		V = null;
		Vk = null;
		left = null;
		right = null;

		return P;
	}

	private double getAveragePreference(Matrix matrix) throws TasteException {
		RunningAverage average = new FullRunningAverage();
		int i = matrix.getColumnLabelBindings().get(String.valueOf(userID));
		for (int j = 0; j < matrix.rowSize(); j++) {
			if (matrix.get(j, i) > 0.0) {
				average.addDatum(matrix.get(j, i));
			}
		}

		return average.getAverage();
	}

	public String getKeysByValue(Map<String, Integer> map, int value) {
		Iterator<Entry<String, Integer>> iter = map.entrySet().iterator();
		String key = "";

		while (iter.hasNext()) {
			Map.Entry<String, Integer> entry = iter.next();
			if (entry.getValue().equals(value)) {
				key = entry.getKey();
			}
		}

		return key;
	}

	private float predictRating(int user, int item) {
		float entry = (float) A.get(item, user);
		float prediction = (float) (entry + average);

		if (prediction <= 1) {
			return 1.0f;
		} else if (prediction > 5) {
			return 5.0f;
		} else {
			return prediction;
		}
	}

	@Override
	public List<RecommendedItem> recommend(long userID, int howMany,
			IDRescorer rescorer) throws TasteException {
		// TODO Auto-generated method stub
		Preconditions.checkArgument(howMany >= 1, "howMany must be at least 1");
		log.debug("Recommending items for user ID '{}'", userID);

		int column = A.getColumnLabelBindings().get(String.valueOf(userID));

		FastIDSet possibleItemIDs = new FastIDSet();
		for (int i = 0; i < A.rowSize(); i++) {
			if (A.get(i, column) <= 0) {
				possibleItemIDs.add(Long.parseLong(getKeysByValue(
						A.getRowLabelBindings(), i)));
			}
		}

		TopItems.Estimator<Long> estimator = new Estimator(userID);

		List<RecommendedItem> topItems = TopItems.getTopItems(howMany,
				possibleItemIDs.iterator(), rescorer, estimator);

		log.debug("Recommendations are: {}", topItems);

		return topItems;
	}

	@Override
	public float estimatePreference(long userID, long itemID)
			throws TasteException {
		// TODO Auto-generated method stub
		Integer useridx = columnLabels.get(String.valueOf(userID));
		if (useridx == null) {
			throw new NoSuchUserException();
		}

		Integer itemidx = rowLabels.get(String.valueOf(itemID));
		if (itemidx == null) {
			throw new NoSuchItemException();
		}
		return predictRating(useridx, itemidx);
	}

	@Override
	public void refresh(Collection<Refreshable> alreadyRefreshed) {
		// TODO Auto-generated method stub

	}

	private final class Estimator implements TopItems.Estimator<Long> {
		private final long theUserID;

		private Estimator(long theUserID) {
			this.theUserID = theUserID;
		}

		@Override
		public double estimate(Long itemID) throws TasteException {
			// TODO Auto-generated method stub
			return estimatePreference(theUserID, itemID);
		}
	}
	
	private void displayMatrixContent(String matrixName, Matrix matrix) {
		System.out.println("Matrix name: " + matrixName);
		System.out.println("Row count: " + matrix.rowSize());
		System.out.println("Column count: " + matrix.columnSize());
		if (matrix.getRowLabelBindings() != null) {
			System.out.println("Row labels: "
					+ matrix.getRowLabelBindings().toString());
		}
		if (matrix.getColumnLabelBindings() != null) {
			System.out.println("Column labels: "
					+ matrix.getColumnLabelBindings().toString());
		}
		for (int i = 0; i < matrix.rowSize(); i++) {
			StringBuilder tempStr = new StringBuilder();
			for (int j = 0; j < matrix.columnSize(); j++) {
				tempStr.append(Double.parseDouble(String.format("%.4f",
						matrix.get(i, j)))
						+ ", ");
			}
			System.out.println(tempStr);
			tempStr = null;
		}
	}
}
